1. Introduction
2. The Base Code
3. Making the mistake - Deadlock

1. Introduction

Deadlock can occur when two or more threads are competing for two or more guarded resources, when each thread holds a lock required by the other. Each thread holds one lock and is blocked, waiting for the other to release its lock.

2. The Base Code

In our example we have two instances of RunnableDeadlock we start in a separate thread, tab and tba . They get a reference to both NumberContainer instances - although in reverse order.

We have a small doHeavyWork() method that thus some arbitrary "work". It serves as a - contrived - example of a heavy workload.

package be.ooxs.examples.threads;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class ThreadDeadlock {

	public static void main(String[] args) throws InterruptedException {
		NumberContainer a = new NumberContainer();
		NumberContainer b = new NumberContainer();
		Thread tab = new Thread(new RunnableDeadlock(a, b, "AB"));
		Thread tba = new Thread(new RunnableDeadlock(b, a, "BA"));
		tab.start();
		tba.start();

		System.out.println("M 1");
		tab.join();
		tba.join();
		System.out.println("M 2");

		System.out.println("A: ");
		a.print();
		System.out.println("B: ");
		b.print();
	}

}

class NumberContainer {
	private List<Number> elements = new ArrayList<Number>();

	public void add(Number number) {
		elements.add(number);
	}

	public void print() {
		System.out.println(elements);
	}
}

class RunnableDeadlock implements Runnable {
	private List<Integer> numbers = new ArrayList<Integer>();
	private NumberContainer n1;
	private NumberContainer n2;
	private String name;

	public RunnableDeadlock(NumberContainer n1, NumberContainer n2, String name) {
		super();
		this.n1 = n1;
		this.n2 = n2;
		this.name = name;
	}

	@Override
	public void run() {
		System.out.println("Lock n1 " + name);
		doHeavyWork();
		System.out.println("Lock n2 " + name);
		n1.add(numbers.get(0));
		n2.add(numbers.get(numbers.size() - 1));
	}

	public void doHeavyWork() {
		//The content of this method is unimportant
		//it just allows the scheduler sufficient time 
		//to intervene
		long start = System.currentTimeMillis();
		System.out.println("start heavy work");
		Random random = new Random();
		for (int i = 0; i < 100000; i++) {
			numbers.add(random.nextInt());
		}
		Collections.sort(numbers);
		long end = System.currentTimeMillis();
		System.out.println("end heavy work: " + (end - start));
	}
}

If we run this we get output more or less as expected. The important part here is to see that

  1. Both threads print "Lock n1 ..." and then both print "Lock n2 ..."
  2. "M 2" and the results are printed by each thread
Lock n1 AB
M 1
Lock n1 BA
start heavy work
start heavy work
end heavy work: 547
Lock n2 BA
end heavy work: 552
Lock n2 AB
M 2
A: 
[2147366574, -2147438543]
B: 
[-2147463273, 2147480393]

3. Making the mistake - Deadlock

Our new version of the method is unchanged, except for both synchronized blocks.

In this case we obtain two locks and then change values on n1 and n2 , but both threads have their NumberContainer in reverse order.

So while thread tab obtains the lock on a and tba obtains the lock on b , tba remains blocked forever waiting for a and tab is blocked until you put it out of its misery, waiting for b .

	@Override
		public void run() {
			System.out.println("Lock n1 " + name);
			synchronized (n1) {
				doHeavyWork();
				System.out.println("Lock n2 " + name);
				synchronized (n2) {
					n1.add(numbers.get(0));
					n2.add(numbers.get(numbers.size() - 1));
				}
			}
		}

Observe that both threads print their "Lock n2 ..." message and then wait forever, each having obtained one lock and waiting for the other. Mutually making each other wait forever.

You must now kill the program manually.

Lock n1 AB
start heavy work
M 1
Lock n1 BA
start heavy work
end heavy work: 522
Lock n2 AB
end heavy work: 554
Lock n2 BA